iT邦幫忙

2025 iThome 鐵人賽

DAY 4
1
Rust

大家一起跟Rust當好朋友吧!系列 第 4

Day 4: 所有權 (Ownership):Rust 最核心的概念!

  • 分享至 

  • xImage
  •  

嗨嗨!大家好!歡迎來到 Rust 三十天挑戰的第四天!

今天我們要來探討 Rust 最核心、也可能是最讓初學者困惑的概念:所有權 (Ownership)。如果說前三天我們學的是 Rust 的「語法」,那麼今天要學的就是 Rust 的「靈魂」。

老實說,當我第一次接觸所有權這個概念時,腦袋裡滿滿都是問號。畢竟在 C# 或 JavaScript 的世界裡,我們很少需要思考「誰擁有這個變數」或「什麼時候記憶體會被釋放」,因為都有垃圾回收器(GC)幫我們處理。但在 Rust 的世界裡,沒有 GC,也不需要我們手動管理記憶體,那記憶體到底是怎麼管理的呢?

答案就是:所有權系統

為什麼需要所有權?

在深入了解所有權之前,讓我們先思考一下記憶體管理的幾種方式:

1. 手動管理(C/C++)

// C 語言的例子
char* message = malloc(100);  // 手動分配記憶體
strcpy(message, "Hello");
// ... 使用 message
free(message);                // 手動釋放記憶體

優點:完全控制,效能極佳
缺點:容易出錯(忘記釋放、重複釋放、使用已釋放的記憶體)

2. 垃圾回收(C#, Java, JavaScript)

// C# 的例子
string message = "Hello";  // 系統自動管理記憶體
// GC 會在適當時機自動回收不再使用的記憶體

優點:程式設計簡單,不易出錯
缺點:執行時開銷、GC 暫停、無法精確控制記憶體釋放時機

3. 所有權系統(Rust)

// Rust 的例子
let message = String::from("Hello");  // 系統自動管理記憶體
// 當 message 離開作用域時,記憶體自動釋放

優點:結合了手動管理的效能與垃圾回收的安全性
缺點:學習曲線較陡峭

所有權的三大規則

Rust 的所有權系統建立在三個簡單但強大的規則之上:

  1. 每個值都有一個所有者(owner)
  2. 同一時間只能有一個所有者
  3. 當所有者離開作用域時,值會被丟棄

讓我們用實際的例子來理解這些規則:

規則 1 & 3:每個值都有所有者,離開作用域就釋放

fn main() {
    {                        // s 還不存在
        let s = "hello";     // s 進入作用域
        // 在這裡使用 s
    }                        // s 離開作用域,被釋放
    
    // println!("{}", s);    // 錯誤!s 已經不存在了
}

這個例子看起來很簡單,但其實展示了 Rust 如何自動管理記憶體。當變數 s 離開作用域時,Rust 會自動呼叫一個特殊的函式 drop,清理該變數占用的記憶體。

規則 2:同一時間只能有一個所有者

這裡就開始有趣了。讓我們看看會發生什麼:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;  // s1 的值被「移動」到 s2
    
    // println!("{}", s1);  // 編譯錯誤!s1 已經無效了
    println!("{}", s2);     // 這個沒問題
}

咦?為什麼 s1 突然就無效了?這就是 Rust 所有權系統的核心:移動語意(Move Semantics)

深入理解移動語意

讓我們比較一下不同型別的行為:

基本型別的複製

fn main() {
    let x = 5;
    let y = x;    // x 被複製到 y
    
    println!("x = {}, y = {}", x, y);  // 都可以使用
}

這裡沒問題,因為像 i32 這樣的基本型別實現了 Copy trait,所以會被複製而不是移動。

複雜型別的移動

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;  // s1 被移動到 s2
    
    // println!("{}", s1);  // 錯誤!s1 已經無效
}

為什麼會這樣?讓我們看看 String 在記憶體中的結構:

fn main() {
    let s1 = String::from("hello");
    // s1 在 stack 上存放:
    // - 指向 heap 記憶體的 pointer
    // - 長度 (5)
    // - 容量 (5)
    //
    // 實際的字串資料 "hello" 存放在 heap 上
    
    let s2 = s1;  // 只複製 stack 上的資料(pointer、長度、容量)
                  // 但 s1 變得無效,避免雙重釋放 (double free)
}

如果 Rust 允許 s1s2 同時有效,那麼當它們都離開作用域時,就會嘗試釋放同一塊堆積記憶體兩次,這會導致「雙重釋放」的錯誤。

函式與所有權

所有權在函式呼叫時也會發生轉移:

fn main() {
    let s = String::from("hello");
    takes_ownership(s);  // s 的所有權轉移給函式
    
    // println!("{}", s);  // 錯誤!s 已經無效
    
    let x = 5;
    makes_copy(x);       // x 被複製,原本的 x 仍然有效
    
    println!("x = {}", x);  // 沒問題
}

fn takes_ownership(some_string: String) {
    println!("{}", some_string);
}  // some_string 離開作用域,記憶體被釋放

fn makes_copy(some_integer: i32) {
    println!("{}", some_integer);
}  // some_integer 離開作用域,但因為是 Copy 型別,沒有特殊處理

回傳值與所有權轉移

函式也可以轉移所有權給呼叫者:

fn main() {
    let s1 = gives_ownership();         // 函式回傳值的所有權轉移給 s1
    let s2 = String::from("hello");     
    let s3 = takes_and_gives_back(s2);  // s2 被移動進函式,回傳值移動給 s3
    
    // println!("{}", s2);  // 錯誤!s2 已經被移動
    println!("{}", s1);     // 沒問題
    println!("{}", s3);     // 沒問題
}

fn gives_ownership() -> String {
    let some_string = String::from("yours");
    some_string  // 回傳值,所有權轉移給呼叫者
}

fn takes_and_gives_back(a_string: String) -> String {
    a_string  // 回傳傳入的值,所有權轉移給呼叫者
}

實戰練習:理解所有權的移動

讓我們寫一個實際的例子來加深理解:## 如何解決所有權轉移的問題?

你可能會想:「這樣不是很麻煩嗎?每次把變數傳給函式就不能再用了?」

確實,如果每次都要轉移所有權,程式會變得很難寫。幸好 Rust 提供了幾種解決方案:

1. 使用 Clone

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();  // 明確地複製資料
    
    println!("s1 = {}, s2 = {}", s1, s2);  // 都可以使用
}

clone() 會建立資料的深度複製,但這有效能成本。

2. 使用引用(明天會學到)

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);  // 傳遞引用,不轉移所有權
    
    println!("'{}' 的長度是 {}", s1, len);  // s1 仍然可以使用
}

fn calculate_length(s: &String) -> usize {
    s.len()
}  // s 是引用,不擁有資料,所以不會釋放記憶體

3. 回傳所有權

fn main() {
    let s1 = String::from("hello");
    let (s2, len) = calculate_length_and_return(s1);
    
    println!("'{}' 的長度是 {}", s2, len);
}

fn calculate_length_and_return(s: String) -> (String, usize) {
    let length = s.len();
    (s, length)  // 回傳所有權
}

小測驗:猜猜看會發生什麼?

看看下面這些程式碼,猜猜哪些會編譯成功,哪些會失敗:

// 程式碼片段 1
fn quiz_1() {
    let x = 5;
    let y = x;
    println!("{} {}", x, y);
}

// 程式碼片段 2  
fn quiz_2() {
    let s = String::from("hello");
    let t = s;
    println!("{} {}", s, t);
}

// 程式碼片段 3
fn quiz_3() {
    let s = String::from("hello");
    takes_string(s);
    println!("{}", s);
}

fn takes_string(s: String) {
    println!("{}", s);
}

// 程式碼片段 4
fn quiz_4() {
    let s = String::from("hello");
    let s = s.clone();
    println!("{}", s);
}

答案:

  • 片段 1:✅ 編譯成功(i32 實現了 Copy)
  • 片段 2:❌ 編譯失敗(s 被移動到 t)
  • 片段 3:❌ 編譯失敗(s 被移動到函式)
  • 片段 4:✅ 編譯成功(clone 建立新的所有權)

今天的收穫

今天我們學會了 Rust 最核心的概念:

所有權的三大規則:

  1. 每個值都有一個所有者
  2. 同一時間只能有一個所有者
  3. 所有者離開作用域時,值被釋放

關鍵概念:

  • 移動語意:複雜型別賦值時會轉移所有權
  • Copy trait:基本型別會被複製而不是移動
  • 函式呼叫:傳參和回傳都會轉移所有權
  • Clone:明確地複製資料來避免移動

為什麼這樣設計?

  • 避免雙重釋放
  • 避免記憶體洩漏
  • 避免懸置指標
  • 零成本抽象(沒有 GC 開銷)

雖然一開始可能覺得限制很多,但這些規則確保了我們的程式在編譯時就沒有記憶體安全問題。一旦習慣了,你會發現這樣的程式碼更容易理解和維護。

今天的小挑戰

寫一個程式,實作以下功能:

  1. 建立一個函式 process_string,接收一個 String
  2. 在函式內為字串加上前綴 "processed: " 和後綴 "!"
  3. 回傳處理後的 String
  4. main 函式中測試這個函式,確保原本的字串在函式呼叫後不能再使用

範例程式碼框架:

fn process_string(/* 在這裡填入參數 */) -> String {
    // 在這裡實作字串處理邏輯
}

fn main() {
    let original = String::from("hello world");
    
    let result = process_string(/* 在這裡傳入參數 */);
    
    println!("處理結果: {}", result);
    
    // 嘗試使用原本的字串(這應該要編譯失敗)
    // println!("原本的字串: {}", original);
}

提示:

  • 思考一下函式參數應該是 String 還是 &String
  • 如果原本的字串在函式呼叫後不能使用,代表發生了什麼?
  • 可以用 format! 巨集來組合字串

期望的輸出:

處理結果: processed: hello world!

而且 println!("原本的字串: {}", original); 這行如果取消註解應該要編譯失敗!

這個挑戰會讓你實際體驗所有權轉移,以及為什麼我們需要引用與借用(明天的主題)!

明天我們將學習引用與借用,這會解決今天遇到的許多所有權轉移的問題,讓我們能更靈活地使用資料而不需要轉移所有權。

如果今天的內容讓你覺得有點頭痛,別擔心,這是完全正常的!所有權是 Rust 最獨特的概念,需要時間消化。多寫幾個例子,很快就會開始有感覺了。

我們明天見!


上一篇
Day 3: 函式與流程控制:讓程式有邏輯、有組織
下一篇
Day 5: 參考 (References) 與借用 (Borrowing):不轉移所有權的資料傳遞
系列文
大家一起跟Rust當好朋友吧!20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言